1 综述
回溯法可以看成是蛮力法的升级版,它从解决问题每一步的所有可能选项里系统的选择出一个可行的解决方案。回溯法非常适合由多个步骤组成的问题,并且每个步骤都有多个选项。当我们在某一步选择了其中一个选项时,就进入下一步,然后面临新的选项。我们就这么重复选择,直至到达最终的状态。
用回溯法解决的问题的所有选项可以形象地用树状结构表示。在某一步有n个可能的选项,那么该步骤可以看成是树状结构中的一个节点,每个选项看成树中节点的连接线,经过这些连接线到达某个节点的n个子节点。树的叶节点对应着终结状态。如果在叶节点的状态满足题目的约束条件,那么我们找到了一个可行的解决方案。
如果在叶节点的状态不满足约束条件,那么只好回溯到它的上一个节点再尝试其他选项。如果上一个节点所有可能的选项都已经试过,并且不能达到满足约束条件的终结状态,则再次回溯到上一个节点。如果所有节点的所有选项都已经尝试过仍然不能到达满足约束条件的终结状态,则该问题无解。
2 算法用法
回溯法按深度优先策略搜索问题的解空间树。首先从根节点出发搜索解空间树,当算法搜索至解空间树的某一节点时,先利用剪枝函数判断该节点是否可行(即能得到问题的解)。如果不可行,则跳过对该节点为根的子树的搜索,逐层向其祖先节点回溯;否则,进入该子树,继续按深度优先策略搜索。
回溯法的基本行为是搜索,搜索过程使用剪枝函数来为了避免无效的搜索。剪枝函数包括两类:①. 使用约束函数,剪去不满足约束条件的路径;②. 使用限界函数,剪去不能得到最优解的路径。
问题的关键在于如何定义问题的解空间,转化成树(即解空间树)。解空间树分为两种:子集树和排列树。两种在算法结构和思路上大体相同。
子集树应用例如0-1背包问题,决定多少物品能放进背包收益最大,最终解是所有物品的一个子集,所以是典型的子集树解空间;另一个种排列数例如走迷宫或者旅行售货员,它需要记录路径上节点先后信息。
3 算法应用
当问题是要求满足某种性质(约束条件)的所有解或最优解时,往往使用回溯法。所以它有“通用解题法”之美誉。
(1)装载问题
(2)0-1背包问题
(3)旅行售货员问题
(4)八皇后问题
(5)迷宫问题
(6)图的m着色问题
4 面试题:矩阵中的路径
4.1 题目
请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串的所有字符的路径。路径可以从矩阵中的任意一格开始,每一步可以在矩阵中向左、右、上、下移动一格。如果一条路径经过了矩阵的某个空格,那么该路径不能再次进入该格子。例如,下面的3×4的矩阵中包含一条字符串“bfce”的路径,但是并不包含一条字符串“abfe”路径,因为第一个字符b占据了第一行第二列的格子,路径不能再次进入这个格子。——剑指offer面试题:12
a b t g
c f c s
j d e h
4.2 分析
这是一个经典的可以用回溯法解决的题。首先,我们从矩阵中任意一个字符出发。假设矩阵中某个格子的字符表示为char_one,并且这个格子将对应于路径上的第i个字符。如果路径上的第i个字符不是char_one,那么其就不该出现在路径上第i个位置,需要退回上个状态i-1重新寻找第i个字符。如果恰好就是第i个字符,那么我们就可以在相邻的格子寻找路径上第i+1个字符。矩阵除边界上格子外相邻都是4个格子。重复这个过程,直到路径上的所有字符都能在矩阵中找到对应为重。整个过程又非常像栈数据结构的入栈与出栈操作。
4.3 代码
#include <cstdio>
#include <string>
#include <stack>
using namespace std;
bool hasPathCore(const char* matrix, int rows, int cols, int row, int col, const char* str, int& pathLength, bool* visited);
bool hasPath(const char* matrix, int rows, int cols, const char* str)
{
if(matrix == nullptr || rows < 1 || cols < 1 || str == nullptr)
return false;
bool *visited = new bool[rows * cols];
memset(visited, 0, rows * cols);
int pathLength = 0;
for(int row = 0; row < rows; ++row)
{
for(int col = 0; col < cols; ++col)
{
if(hasPathCore(matrix, rows, cols, row, col, str,
pathLength, visited))
{
return true;
}
}
}
delete[] visited;
return false;
}
bool hasPathCore(const char* matrix, int rows, int cols, int row,
int col, const char* str, int& pathLength, bool* visited)
{
if(str[pathLength] == '\0')
return true;
bool hasPath = false;
if(row >= 0 && row < rows && col >= 0 && col < cols
&& matrix[row * cols + col] == str[pathLength]
&& !visited[row * cols + col])
{
++pathLength;
visited[row * cols + col] = true;
hasPath = hasPathCore(matrix, rows, cols, row, col - 1,
str, pathLength, visited)
|| hasPathCore(matrix, rows, cols, row - 1, col,
str, pathLength, visited)
|| hasPathCore(matrix, rows, cols, row, col + 1,
str, pathLength, visited)
|| hasPathCore(matrix, rows, cols, row + 1, col,
str, pathLength, visited);
if(!hasPath)
{
--pathLength;
visited[row * cols + col] = false;
}
}
return hasPath;
}
——参考自剑指offer
4.4 测试
int main()
{
const char* matrix = "ABCEHJIGSFCSLOPQADEEMNOEADIDEJFMVCEIFGGS";
const char* str = "SGGFIECVAASABCEEJIGOEM";
Test("Test6", (const char*) matrix, 5, 8, str, false);
}
参考自:
算法入门6:回溯法
剑指offer